﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Text;
using System.Data;
using System.Data.Objects;
using System.Data.Objects.DataClasses;
using System.Linq.Expressions;
using System.Linq;
using System.Data.EntityClient;
using System.ComponentModel;
using System.Reflection;

namespace Kocic.RepositoryManagement.EntityFramework4
{
    /// <summary>
    /// Defines methods and operations for managers to perform create, read, update, and delete operations on a entity object, in a object context, through ADO.NET Entity Framework 4.
    /// </summary>
    /// <typeparam name="TEntityObject">The type of the entity object that the entity data manager operations will be implemented for.</typeparam>
    /// <typeparam name="TContext">The type of the object context that will be used by the entity data managers.</typeparam>
    public abstract class EntityRepositoryManager<TEntityObject, TContext> : DataManager<TEntityObject, TContext>, IRetrievable<TEntityObject>
        where TEntityObject : EntityObject
        where TContext : ObjectContext, new()
    {
        /// <summary>
        /// Creates an instance of the EntityDataManager class.
        /// </summary>
        /// <param name="context">An instance of the object context that will be used for operations.</param>
        /// <exception cref="ArgumentNullException"></exception>
        protected EntityRepositoryManager(TContext context)
            : base(context)
        {

        }

        /// <summary>
        /// Creates an instance of the EntityDataManager class. Invoking this constructor will pass the static instance reference to the object context referenced an instance of the EntityExecutionContext class.
        /// </summary>
        protected EntityRepositoryManager()
            : this(EntityExecutionContext<TContext>.CurrentDataSource)
        {

        }

        /// <summary>
        /// Disposes unmanaged resources.
        /// </summary>
        /// <param name="disposing">Value indicating if to invoke the method from the IDisposable.Dispose implementation or from the finalizer.</param>
        protected override void Dispose(Boolean disposing)
        {
            if (disposing)
            {
                if (this.IsDisposed)
                {
                    return;
                }

                /*Checks to see if the context was set in this fashion. If yes, then we do not dispose of the context yet. */
                if (EntityExecutionContext<TContext>.CurrentDataSource != null)
                {
                    this.DisposeDataSource = false;
                }

                if (this.DisposeDataSource)
                {
                    if (this.DataSource.Connection.State == ConnectionState.Open)
                    {
                        this.DataSource.Connection.Close();
                    }

                    this.DataSource.Dispose();
                }

                base.Dispose(disposing);
            }
        }

        /// <summary>
        /// Saves changes to the object context and commits them to the database.
        /// </summary>
        /// <param name="entityObject">The entity object to commit to the database.</param>
        /// <exception cref="ArgumentNullException"></exception>
        protected override void Commit(TEntityObject entityObject)
        {
            if (entityObject == null)
            {
                throw new ArgumentNullException("entityObject", "Entity object is null.");
            }

            try
            {
                this.DataSource.SaveChanges();
            }
            catch (OptimisticConcurrencyException)
            {
                this.DataSource.Refresh(RefreshMode.ClientWins, entityObject);
                this.DataSource.SaveChanges();
            }
            catch
            {
                throw;
            }
        }

        /// <summary>
        /// Performs operations to add the entity object to the object context.
        /// </summary>
        /// <param name="entityObject">The entity object to add.</param>
        protected override void DataSourceCreateOperations(TEntityObject entityObject)
        {
            this.DataSource.AddObject(entityObject);
        }

        /// <summary>
        /// Performs operations to update the entity object in the object context.
        /// </summary>
        /// <param name="entityObject">The entity object to update.</param>
        protected override void DataSourceUpdateOperations(TEntityObject entityObject)
        {
            //None implemented for entity framework, since no explicit update command exists.
        }

        /// <summary>
        /// Performs operations to remove the entity object from the object context.
        /// </summary>
        /// <param name="entityObject">The entity object to remove.</param>
        protected override void DataSourceDeleteOperations(TEntityObject entityObject)
        {
            this.DataSource.DeleteObject(entityObject);
        }

        /// <summary>
        /// Checks to see if a EntityContextDeferrer instance was instantiated and a reference exists.
        /// </summary>
        /// <returns>Value indicating if the EntityContextDeferrer was instantiated and a reference exists.</returns>
        protected override bool IsDeferralInstanceInstantiated()
        {
            return EntityContextDeferrer<TContext>.DeferralOperation != null;
        }

        /// <summary>
        /// Executes any deferral operations for the provided entity object.
        /// </summary>
        /// <param name="entityObject">The entity object to defer operations on.</param>
        protected override void DeferrerOperations(TEntityObject entityObject)
        {
            if (this.IsDeferralInstanceInstantiated())
            {
                EntityContextDeferrer<TContext>.DeferredObjects.Add(entityObject);
            }
        }

        /// <summary>
        /// Performs any additional operations with the data object before it is created and added to the context.
        /// </summary>
        /// <param name="entityObject">The data object used in performing additional operations to prior to adding and saving the changes to the context.</param>
        protected override TEntityObject PreCreate(TEntityObject entityObject)
        {
            return null;
        }

        /// <summary>
        /// Performs any additional operations with the data object before it is updated and its changes saved to the context.
        /// </summary>
        /// <param name="entityObject">The data object used in performing additional operations to prior to saving its changes to the context.</param>
        protected override TEntityObject PreUpdate(TEntityObject entityObject)
        {
            return null;
        }

        /// <summary>
        /// Performs any additional operations with the data object before it is deleted and removed from the context.
        /// </summary>
        /// <param name="entityObject">The data object used in performing additional operations to prior to deleting it from the context and saving the changes.</param>
        protected override TEntityObject PreDelete(TEntityObject entityObject)
        {
            return null;
        }

        #region IRetrievable<TEntityObject> Members

        /*These methods have been marked as abstract since the manager must be required to provide the basic read functionality */

        /// <summary>
        /// Defines how to return a single instance of the entity object based on the specified search value.
        /// </summary>
        /// <param name="searchValue">An object used to store particular search criteria.</param>
        /// <returns>The entity object that is a result of a search.</returns>
        public abstract TEntityObject RetrieveSingle(Object searchValue);

        /// <summary>
        /// Defines how to retrieve multiple instances of the entity object.
        /// </summary>
        /// <returns>An enumerator of a collection of type TEntityObject.</returns>
        public abstract IEnumerable<TEntityObject> RetrieveMultiple();

        /// <summary>
        /// Defines how to retrieve multiple instances of the entity object based on the specified search value.
        /// </summary>
        /// <param name="searchValue">An object used to store particular search criteria.</param>
        /// <returns>An enumerator of a collection of type TEntityObject that is the result of a search.</returns>
        public abstract IEnumerable<TEntityObject> RetrieveMultiple(Object searchValue);

        #endregion

        /// <summary>
        /// Check to see if the state of an entity object is in the detached state or if its object state entry can be located in the ObjectStateManager.
        /// </summary>
        /// <param name="entityObject">The entity object to check the state.</param>
        /// <returns>Value indiciating if the entity object is detached or not it doesn't have an entry in the ObjectStateManager.</returns>
        protected override Boolean? IsDataObjectInRemovedState(TEntityObject entityObject)
        {
            if (entityObject.EntityState.Equals(EntityState.Detached))
            {
                return true;
            }
            else if (entityObject.EntityKey == null)
            {
                return true;
            }
            else
            {
                ObjectStateEntry entry;

                return !this.DataSource.ObjectStateManager.TryGetObjectStateEntry(entityObject.EntityKey, out entry);
            }

        }

        /// <summary>
        /// Saves a entity object to the context and the database. Either creates or updates the entity object based on the object's entity state. If an instance of the
        /// EntityContextDeferral instance exists, then the entity object is simply added to the context and no changes are saved.
        /// </summary>
        /// <param name="entityObject">The entity object to save to the context and database.</param>
        /// <param name="context">A instance of the object context to which the entity object will be saved to. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the EntityExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        public static void SaveEntityObject(TEntityObject entityObject, TContext context = null)
        {
            EntityRepositoryManager<TEntityObject, TContext>.ContextOperations(entityObject, context, (inEntityObject, inContext) =>
            {
                ObjectStateEntry entry;

                if (inEntityObject.EntityState.Equals(EntityState.Detached) || !inContext.ObjectStateManager.TryGetObjectStateEntry(entityObject.EntityKey, out entry))
                {
                    inContext.AddObject(inEntityObject);
                }
            });
        }

        /// <summary>
        /// Retrieves and returns IQueryable&lt;TEntityObject> object based on the provided where expression.
        /// </summary>
        /// <param name="whereExpression">Where expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the EntityExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;TEntityObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static IQueryable<TEntityObject> Retrieve(Expression<Func<TEntityObject, Boolean>> whereExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = EntityExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the EntityExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            return currentContext.CreateObjectSet<TEntityObject>().Where(whereExpression);
        }

        /// <summary>
        /// Retrieves and returns IQueryable&lt;TEntityObject> object based on the provided select expression.
        /// </summary>
        /// <param name="retrieveExpression">Retrieve expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the EntityExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <returns>IQueryable&lt;TEntityObject> object.</returns>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static IQueryable<TEntityObject> Retrieve(Expression<Func<TEntityObject, TEntityObject>> retrieveExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = EntityExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the EntityExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            return currentContext.CreateObjectSet<TEntityObject>().Select(retrieveExpression);
        }

        /// <summary>
        /// Removes a data object from the object context and the database. If an instance of the
        /// EntityContextDeferral instance exists, then the entity object is simply removed and no changes are saved.
        /// </summary>
        /// <param name="entityObject">The entity object to delete and remove from the object context.</param>
        /// <param name="context">A instance of the object context to which the entity object will be removed from. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the EntityExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void DeleteEntityObject(TEntityObject entityObject, TContext context = null)
        {
            EntityRepositoryManager<TEntityObject, TContext>.ContextOperations(entityObject, context, (inEntityObject, inContext) =>
            {
                ObjectStateEntry entry;

                Boolean isRemoved = inEntityObject.EntityState.Equals(EntityState.Detached) || !inContext.ObjectStateManager.TryGetObjectStateEntry(entityObject.EntityKey, out entry);

                if (isRemoved)
                {
                    throw new InvalidOperationException("The entity object is in a detached state or does not have a state entry, and cannot be deleted.");
                }
                else
                {
                    inContext.DeleteObject(inEntityObject);
                }
            });
        }

        /// <summary>
        /// Deletes entity objects based on the provided where expression.
        /// </summary>
        /// <param name="whereExpression">Where expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the EntityExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DeleteEntityObjects(Expression<Func<TEntityObject, Boolean>> whereExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = EntityExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the EntityExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            var objects = currentContext.CreateObjectSet<TEntityObject>().Where(whereExpression).ToList();

            foreach (var o in objects)
            {
                EntityRepositoryManager<TEntityObject, TContext>.DeleteEntityObject(o);
            }
        }

        /// <summary>
        /// Deletes entity objects based on the provided select expression.
        /// </summary>
        /// <param name="retrieveExpression">Retrieve expression.</param>
        /// <param name="context">A instance of the object context to which all the entity objects will be pulled from. If this instance is null, then a object context instance referenced
        /// by a EntityExecutionContext instance will be used. If this instance is not null and an instance of the EntityExecutionContext exists within the scope call of this method, an exception will be thrown.</param>
        /// <exception cref="InvalidOperationException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        public static void DeleteEntityObjects(Expression<Func<TEntityObject, TEntityObject>> retrieveExpression, TContext context = null)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = EntityExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the EntityExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not been instantiated.");
            }

            var objects = currentContext.CreateObjectSet<TEntityObject>().Select(retrieveExpression).ToList();

            foreach (var o in objects)
            {
                EntityRepositoryManager<TEntityObject, TContext>.DeleteEntityObject(o);
            }
        }

        /// <summary>
        /// Helper method which has the same logic when trying to add/remove entity objects in/from a context. Because the same logic is used, a delegate to the context's
        /// AddObject/DeleteObject methods is used.
        /// </summary>
        /// <param name="entityObject">The entity object for which the appropriate action will take place.</param>
        /// <param name="context">The object context for which the action upon the entity object will take place.</param>
        /// <param name="contextAction">A delegate to a method that will execute the appropriate context operations.</param>
        private static void ContextOperations(TEntityObject entityObject, TContext context, Action<TEntityObject, TContext> contextAction)
        {
            TContext currentContext;

            if (context != null)
            {
                currentContext = context;
            }
            else if (EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                currentContext = EntityExecutionContext<TContext>.CurrentDataSource;
            }
            else if (context != null && EntityExecutionContext<TContext>.CurrentDataSource != null)
            {
                throw new InvalidOperationException("Unable to use the provided object context in one of the arguments, since an instance of the EntityExecutionContext already exists within the execution scope of this method.");
            }
            else
            {
                throw new InvalidOperationException("An instance of the object context has not bee instantiated.");
            }

            bool toDefer = EntityContextDeferrer<TContext>.DeferralOperation != null ? true : false;

            try
            {
                contextAction(entityObject, currentContext);

                if (!toDefer)
                {
                    currentContext.SaveChanges();
                }
                else
                {
                    EntityContextDeferrer<TContext>.DeferredObjects.Add(entityObject);
                }
            }
            catch (OptimisticConcurrencyException)
            {
                currentContext.Refresh(RefreshMode.ClientWins, entityObject);
                currentContext.SaveChanges();
            }
        }
    }
}